msg_tool\scripts\hexen_haus\archive/
odio.rs

1//! HexenHaus ODIO archive (.bin)
2use crate::ext::io::*;
3use crate::scripts::base::*;
4use crate::types::*;
5use anyhow::{Result, anyhow};
6use std::io::{Read, Seek, SeekFrom};
7use std::sync::{Arc, Mutex};
8
9const ODIO_SIGNATURE: &[u8; 4] = b"ODIO";
10const HEADER_CHECK_OFFSET: u64 = 0x0A;
11const HEADER_CHECK_VALUE: u32 = 0xCCAE_01FF;
12const INDEX_START: u64 = 0x12;
13const INDEX_ENTRY_SIZE: u64 = 6;
14const ENTRY_HEADER_SIZE: u64 = 0x2C;
15
16#[derive(Debug)]
17/// HexenHaus ODIO archive builder
18pub struct HexenHausOdioArchiveBuilder;
19
20impl HexenHausOdioArchiveBuilder {
21    /// Creates a new `HexenHausOdioArchiveBuilder`
22    pub const fn new() -> Self {
23        HexenHausOdioArchiveBuilder
24    }
25}
26
27impl ScriptBuilder for HexenHausOdioArchiveBuilder {
28    fn default_encoding(&self) -> Encoding {
29        Encoding::Cp932
30    }
31
32    fn default_archive_encoding(&self) -> Option<Encoding> {
33        Some(Encoding::Cp932)
34    }
35
36    fn build_script(
37        &self,
38        buf: Vec<u8>,
39        _filename: &str,
40        _encoding: Encoding,
41        archive_encoding: Encoding,
42        config: &ExtraConfig,
43        _archive: Option<&Box<dyn Script>>,
44    ) -> Result<Box<dyn Script>> {
45        Ok(Box::new(HexenHausOdioArchive::new(
46            MemReader::new(buf),
47            archive_encoding,
48            config,
49        )?))
50    }
51
52    fn build_script_from_file(
53        &self,
54        filename: &str,
55        _encoding: Encoding,
56        archive_encoding: Encoding,
57        config: &ExtraConfig,
58        _archive: Option<&Box<dyn Script>>,
59    ) -> Result<Box<dyn Script>> {
60        if filename == "-" {
61            let data = crate::utils::files::read_file(filename)?;
62            return Ok(Box::new(HexenHausOdioArchive::new(
63                MemReader::new(data),
64                archive_encoding,
65                config,
66            )?));
67        }
68        let file = std::fs::File::open(filename)?;
69        let reader = std::io::BufReader::new(file);
70        Ok(Box::new(HexenHausOdioArchive::new(
71            reader,
72            archive_encoding,
73            config,
74        )?))
75    }
76
77    fn build_script_from_reader(
78        &self,
79        reader: Box<dyn ReadSeek>,
80        _filename: &str,
81        _encoding: Encoding,
82        archive_encoding: Encoding,
83        config: &ExtraConfig,
84        _archive: Option<&Box<dyn Script>>,
85    ) -> Result<Box<dyn Script>> {
86        Ok(Box::new(HexenHausOdioArchive::new(
87            reader,
88            archive_encoding,
89            config,
90        )?))
91    }
92
93    fn extensions(&self) -> &'static [&'static str] {
94        &["bin"]
95    }
96
97    fn script_type(&self) -> &'static ScriptType {
98        &ScriptType::HexenHausOdio
99    }
100
101    fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option<u8> {
102        if buf_len >= ODIO_SIGNATURE.len() && buf.starts_with(ODIO_SIGNATURE) {
103            Some(10)
104        } else {
105            None
106        }
107    }
108
109    fn is_archive(&self) -> bool {
110        true
111    }
112}
113
114#[derive(Debug, Clone)]
115struct HexenHausOdioEntry {
116    name: String,
117    offset: u64,
118    size: u64,
119}
120
121#[derive(Debug)]
122/// HexenHaus ODIO archive reader
123pub struct HexenHausOdioArchive<T: Read + Seek + std::fmt::Debug> {
124    reader: Arc<Mutex<T>>,
125    entries: Vec<HexenHausOdioEntry>,
126}
127
128impl<T: Read + Seek + std::fmt::Debug> HexenHausOdioArchive<T> {
129    /// Creates a new `HexenHausOdioArchive`
130    pub fn new(mut reader: T, _archive_encoding: Encoding, _config: &ExtraConfig) -> Result<Self> {
131        reader.seek(SeekFrom::Start(0))?;
132        let mut signature = [0u8; 4];
133        reader.read_exact(&mut signature)?;
134        if signature != *ODIO_SIGNATURE {
135            return Err(anyhow!("Invalid HexenHaus ODIO signature"));
136        }
137
138        let reserved = reader.read_u32()?;
139        if reserved != 0 {
140            return Err(anyhow!("Unexpected reserved field in ODIO header"));
141        }
142
143        reader.seek(SeekFrom::Start(HEADER_CHECK_OFFSET))?;
144        let header_check = reader.read_u32()?;
145        if header_check != HEADER_CHECK_VALUE {
146            return Err(anyhow!("Invalid HexenHaus ODIO header check value"));
147        }
148
149        let file_length = reader.seek(SeekFrom::End(0))?;
150        reader.seek(SeekFrom::Start(INDEX_START))?;
151        let first_offset = u64::from(reader.read_u32()?);
152        if first_offset < INDEX_START {
153            return Err(anyhow!("First entry offset precedes index start"));
154        }
155        if first_offset > file_length {
156            return Err(anyhow!("First entry offset exceeds file length"));
157        }
158
159        let index_len = first_offset
160            .checked_sub(INDEX_START)
161            .ok_or_else(|| anyhow!("Invalid index length in ODIO archive"))?;
162        if index_len % INDEX_ENTRY_SIZE != 0 {
163            return Err(anyhow!("ODIO index length is not aligned"));
164        }
165        let entry_count_u64 = index_len / INDEX_ENTRY_SIZE;
166        let entry_count =
167            usize::try_from(entry_count_u64).map_err(|_| anyhow!("ODIO entry count overflow"))?;
168        if entry_count == 0 {
169            return Err(anyhow!("ODIO archive contains no entries"));
170        }
171
172        let mut entries = Vec::with_capacity(entry_count);
173        let mut index_offset = INDEX_START;
174        let mut next_offset = first_offset;
175
176        for i in 0..entry_count {
177            let entry_offset = next_offset;
178
179            index_offset = index_offset
180                .checked_add(INDEX_ENTRY_SIZE)
181                .ok_or_else(|| anyhow!("Index offset overflow"))?;
182
183            if i + 1 == entry_count {
184                next_offset = file_length;
185            } else {
186                if index_offset + 4 > file_length {
187                    return Err(anyhow!("Index offset exceeds file length"));
188                }
189                reader.seek(SeekFrom::Start(index_offset))?;
190                next_offset = u64::from(reader.read_u32()?);
191            }
192
193            if entry_offset > file_length {
194                return Err(anyhow!("Entry offset exceeds file length"));
195            }
196            if next_offset > file_length {
197                return Err(anyhow!("Entry extends beyond file length"));
198            }
199            if next_offset < entry_offset {
200                return Err(anyhow!("Entry offsets are out of order"));
201            }
202
203            let size = next_offset - entry_offset;
204            if size == 0 {
205                continue;
206            }
207
208            let name = format!("{:04}.ogg", i);
209            entries.push(HexenHausOdioEntry {
210                name,
211                offset: entry_offset,
212                size,
213            });
214        }
215
216        if entries.is_empty() {
217            return Err(anyhow!("ODIO archive contains no readable entries"));
218        }
219
220        reader.seek(SeekFrom::Start(0))?;
221        Ok(HexenHausOdioArchive {
222            reader: Arc::new(Mutex::new(reader)),
223            entries,
224        })
225    }
226}
227
228impl<T: Read + Seek + std::fmt::Debug + std::any::Any> Script for HexenHausOdioArchive<T> {
229    fn default_output_script_type(&self) -> OutputScriptType {
230        OutputScriptType::Json
231    }
232
233    fn default_format_type(&self) -> FormatOptions {
234        FormatOptions::None
235    }
236
237    fn is_archive(&self) -> bool {
238        true
239    }
240
241    fn iter_archive_filename<'a>(
242        &'a self,
243    ) -> Result<Box<dyn Iterator<Item = Result<String>> + 'a>> {
244        Ok(Box::new(
245            self.entries.iter().map(|entry| Ok(entry.name.clone())),
246        ))
247    }
248
249    fn iter_archive_offset<'a>(&'a self) -> Result<Box<dyn Iterator<Item = Result<u64>> + 'a>> {
250        Ok(Box::new(self.entries.iter().map(|entry| Ok(entry.offset))))
251    }
252
253    fn open_file<'a>(&'a self, index: usize) -> Result<Box<dyn ArchiveContent + 'a>> {
254        if index >= self.entries.len() {
255            return Err(anyhow!(
256                "Index out of bounds: {} (total files: {})",
257                index,
258                self.entries.len()
259            ));
260        }
261        let entry = self.entries[index].clone();
262
263        let decrypt = if entry.size >= ENTRY_HEADER_SIZE {
264            let mut header = [0u8; 4];
265            let mut guard = self
266                .reader
267                .lock()
268                .map_err(|e| anyhow!("Failed to lock reader: {}", e))?;
269            guard.seek(SeekFrom::Start(entry.offset))?;
270            guard.read_exact(&mut header)?;
271            header == *b"ONCE"
272        } else {
273            false
274        };
275
276        let (data_offset, data_size) = if decrypt {
277            let data_offset = entry
278                .offset
279                .checked_add(ENTRY_HEADER_SIZE)
280                .ok_or_else(|| anyhow!("Entry data offset overflow"))?;
281            let data_size = entry
282                .size
283                .checked_sub(ENTRY_HEADER_SIZE)
284                .ok_or_else(|| anyhow!("Entry data size underflow"))?;
285            (data_offset, data_size)
286        } else {
287            (entry.offset, entry.size)
288        };
289
290        Ok(Box::new(OdioEntry {
291            name: entry.name,
292            reader: self.reader.clone(),
293            data_offset,
294            data_size,
295            pos: 0,
296            decrypt,
297        }))
298    }
299}
300
301struct OdioEntry<T: Read + Seek> {
302    name: String,
303    reader: Arc<Mutex<T>>,
304    data_offset: u64,
305    data_size: u64,
306    pos: u64,
307    decrypt: bool,
308}
309
310impl<T: Read + Seek> ArchiveContent for OdioEntry<T> {
311    fn name(&self) -> &str {
312        &self.name
313    }
314}
315
316impl<T: Read + Seek> Read for OdioEntry<T> {
317    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
318        let total_size = self.data_size;
319        if self.pos >= total_size {
320            return Ok(0);
321        }
322
323        let remaining = total_size - self.pos;
324        let remaining_usize = match usize::try_from(remaining) {
325            Ok(value) => value,
326            Err(_) => usize::MAX,
327        };
328        let to_read = remaining_usize.min(buf.len());
329        if to_read == 0 {
330            return Ok(0);
331        }
332
333        let absolute_offset = match self.data_offset.checked_add(self.pos) {
334            Some(offset) => offset,
335            None => {
336                return Err(std::io::Error::new(
337                    std::io::ErrorKind::InvalidInput,
338                    "Read position overflow",
339                ));
340            }
341        };
342
343        let mut guard = self.reader.lock().map_err(|e| {
344            std::io::Error::new(
345                std::io::ErrorKind::Other,
346                format!("Failed to lock mutex: {}", e),
347            )
348        })?;
349        guard.seek(SeekFrom::Start(absolute_offset))?;
350        let bytes_read = guard.read(&mut buf[..to_read])?;
351        drop(guard);
352
353        if self.decrypt {
354            for byte in &mut buf[..bytes_read] {
355                *byte = byte.rotate_right(4);
356            }
357        }
358
359        self.pos = self.pos.saturating_add(bytes_read as u64);
360        Ok(bytes_read)
361    }
362}
363
364impl<T: Read + Seek> Seek for OdioEntry<T> {
365    fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
366        let new_pos = match pos {
367            SeekFrom::Start(offset) => offset,
368            SeekFrom::End(offset) => {
369                let size = i64::try_from(self.data_size).map_err(|_| {
370                    std::io::Error::new(
371                        std::io::ErrorKind::InvalidInput,
372                        "Data size exceeds seek range",
373                    )
374                })?;
375                let target = size.checked_add(offset).ok_or_else(|| {
376                    std::io::Error::new(
377                        std::io::ErrorKind::InvalidInput,
378                        "Seek from end caused overflow",
379                    )
380                })?;
381                if target < 0 {
382                    return Err(std::io::Error::new(
383                        std::io::ErrorKind::InvalidInput,
384                        "Seek from end before start",
385                    ));
386                }
387                target as u64
388            }
389            SeekFrom::Current(offset) => {
390                let current = i64::try_from(self.pos).map_err(|_| {
391                    std::io::Error::new(
392                        std::io::ErrorKind::InvalidInput,
393                        "Current position overflow",
394                    )
395                })?;
396                let target = current.checked_add(offset).ok_or_else(|| {
397                    std::io::Error::new(
398                        std::io::ErrorKind::InvalidInput,
399                        "Seek from current caused overflow",
400                    )
401                })?;
402                if target < 0 {
403                    return Err(std::io::Error::new(
404                        std::io::ErrorKind::InvalidInput,
405                        "Seek before start",
406                    ));
407                }
408                target as u64
409            }
410        };
411        self.pos = new_pos;
412        Ok(self.pos)
413    }
414
415    fn stream_position(&mut self) -> std::io::Result<u64> {
416        Ok(self.pos)
417    }
418}